fix(objectql,runtime,rest): store a serializable manifest projection in the package registry - #14499
Conversation
…e registry Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the door projections Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ckages-serializable-manifest
…packages-domain projection Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ckages-serializable-manifest # Conflicts: # packages/runtime/src/domains/packages.ts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 37 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 50d9fd8153b16a9dd058730a4e02f89ac304578d && git checkout 50d9fd8153b16a9dd058730a4e02f89ac304578d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 6961d6ec9bc74651928d56022bc909a7f7cc1a81 && git checkout -B drift-repro 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9 && git merge --no-ff 6961d6ec9bc74651928d56022bc909a7f7cc1a81
node scripts/docs-audit/affected-docs.mjs --json 13bf05d3fe8b1795f3f5aee7daf29ee217590ab9
|
…d land it on sendError Re-homed under the domain seat's serial fence: `packages/rest/src/package-routes.ts` is held by the open PR #14499, so this no longer edits that file. The REST direct-mount doors are covered from the producer side instead. `sendError` (`@objectstack/types`) is the single writer for every nested-envelope error in the repo, and every catch in the package registrar ends there — so wiring the rule at that one exit covers those doors with no per-door call, and covers any door added later by construction. That also puts the helper in the same package as `resolveThrownHttpError`, on the same argument: a rule two doors must agree on cannot live inside one of them. `@objectstack/observability` and `packages/rest` are back to origin/main byte-for-byte, as is the system-context census page (its line rot was caused by the package-routes import this drops). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33663028874 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…stem-write-sharing-materialization Resolves the single CONFLICT (content) in content/docs/permissions/system-context.mdx (the os-regen driver flags this file as MIXED — generated anchors plus hand-written prose — so it text-merges instead of taking one side whole). Row 37 (share-link policy `enabled` check, isSystem) diverged: this branch's copy already carried the #14033 prose ("resolution is not bypassed since #14033 ... a link minted this way does not resolve until the block is enabled"), added when 13bf05d landed on both branches. main's copy had reverted to the pre-#14033 wording via an unrelated commit (8ce628a, "store a serializable manifest projection in the package registry (#14499)") whose own docs-regen step appears to have run against a stale pre-#14033 checkout of this file. Kept this branch's row 37 text: packages/plugins/plugin-sharing/src/ share-link-service.ts:652-697 (the `[#14033]` docblock and the `if (!policy.enabled)` redemption-time check it documents, citing the maintainer's 2026-09-01 ruling and this exact ledger row by number) confirms it is what the current source does, not the reverted main copy. Row 36 differed only in its generated anchor line number (1088 vs 1077) — anchor numbers are recomputed by `pnpm gen:system-context-census` below, so either side was equivalent; kept this branch's. Anchors re-derived on the merged tree with `pnpm gen:system-context-census` in a follow-up commit, per the os-regen driver's documented recovery for this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Fixes #14309
SchemaRegistry.installPackagenow stores a serializable projection of themanifest instead of the caller's live
defineStack()object, and the/packagesread doors project a registry entry onto its declared record fields instead of
spreading it whole.
The measured defect
Reproduced on this branch's base, showcase on a random high port
(
objectstack dev --seed-admin --fresh -p 39117), signed in as the seeded admin:GET /api/v1/packagesINTERNAL_ERRORGET /api/v1/packages/com.example.showcaseINTERNAL_ERRORGET /api/v1/meta/package/com.example.showcaseGET /api/v1/meta/package/com.objectstack.setupError body of the first two:
Which key actually reached the engine
Rather than reason about it, the running server was instrumented at
SchemaRegistry.getPackage(temporary, env-gated, reverted — the file isbyte-identical to its committed state,
git hash-objectverified) to classifyevery top-level manifest key of every installed package by whether it
survives
JSON.stringify. Across the 26 packages a showcase boot installs,exactly one key on one package failed:
And only after plugin init. The same manifest was probed 40 times during
boot with
UNSERIALIZABLE=[]and 381 times afterwards with thepluginsfailure — the transition is a single clean split in the log. That rules out a
whole family of "fixes": an install-time
JSON.stringifyprobe, or any checkthat runs while the record is being written, would have called this record
healthy. The projection therefore drops by shape, not by trial
serialisation, and does not depend on when it is asked.
The repair, at the producer
installPackagestored the argument verbatim aspkg.manifest. The registryitem is a record, not the runtime, so it now holds only the declarative
half. The rule is structural rather than a
pluginsdenylist, because the faultis not "the key is called
plugins" — it is "a live object reached the record":Dateare data and survive;JSON.stringify; a bigint would have thrown);Map,Setand other exotic objects are dropped;self-referencing manifest degrades to a missing field instead of throwing.
Nothing downstream loses a member it was using: the kernel keeps the live object
in
ObjectQL.manifests, and the only reader ofmanifest.plugins[]—registerApp's nested-plugin seam — reads its own parameter, never therecord (grepped, and pinned). The caller's manifest is copied, never stripped in
place, which the pins assert both ways.
⛔ Not a consumer-side tolerance (AGENTS.md Prime Directive #12): no reader is
taught to survive an unserializable record — the record is never unserializable.
Premise correction: which handler answered the 500
The card attributes the list/detail 500 to
packages/rest/src/package-routes.ts{ ...item, source: 'registry' }. Measured, that is not the handler thatanswered in the showcase composition —
packages/runtime/src/domains/packages.tsdid. Two independent probes agree:
GET /api/v1/packages/no.such.packagereturnedPackage 'no.such.package' not found(the runtime domain's spelling); theREST twin says
Package "no.such.package" was not found.GET /api/v1/packages/com.objectstack.setupreturned theinstalled-package record directly under
data(deps.success(pkg)); theREST twin wraps it as
data.packageand stampssource.So the route-level defence is applied at both twins, and
packages/runtime/src/domains/packages.tsis a declared addition to this card'sfile surface (announced on the issue before the work landed). Applying it only
where the card pointed would have shipped a defence with a hole exactly where the
defect was observed.
The doors now project onto the declared
InstalledPackageSchemafields, so anundeclared member appearing on the registry item — a live handle, a
back-reference — degrades to a field the response never mentions, instead of one
bad member on one package failing the whole list for every caller. The REST
merge's database half is deliberately left spreading: its shape belongs to
PackageService, and narrowing it would be a contract decision this card doesnot carry. On the REST side
_diagnosticsis carried explicitly, becausedecorateMetadataItemgrafts it onto every item leavinggetMetaItemsand it ismeasured to be the only thing that decoration adds for
type: 'package'.No response field is added or renamed; responses that already served fine are
byte-identical.
Verification
git rev-parse --short HEADat the time of the runs below:5b8b76e06.Live routes, same showcase boot, after the fix — the table above;
GET /packagesreturns all 26 packages, the showcase manifest keeps its 40 declarative keys
(24 objects, 29 flows, 1 app),
pluginsserves as[]and the function-valuedonEnableis gone.JSON.stringifyof the whole listing succeeds.Package suites (all green, on the commit named above):
New pins — 20 cases over three files:
packages/objectql/src/registry-package-manifest-serializable.test.ts(9),packages/runtime/src/domains/packages-serializable-response.test.ts(5),packages/rest/src/package-registry-item-projection.test.ts(6).Typecheck:⚠️ Measured rather than assumed: each package's
@objectstack/objectql,@objectstack/runtime,@objectstack/restall exit 0.
tsc --noEmitprogram excludes
**/*.test.ts, so--listFilesreports 0 hits for all threenew test files there. The test layer is covered separately —
objectqlandrestrun
check:test-typecheckovertsconfig.test.json, whose--listFilesdoesname the new files (1 hit each, 0 errors attributed to them).
packages/runtimehas no test-typecheck wiring at all, so its new test file is covered by no gate;
it was type-checked standalone (
tsc --noEmit --ignoreConfig --strict, exit 0, 0errors) and that gap is reported to the PM rather than papered over.
Gate families — 62 derived by
node scripts/pm/dispatch-gates.mjs --commands(no paths passed; it takes its own change set from the merge base). 60 ran green
locally. The remaining two exited 3, which each script's own verdict text
declares is NOT MEASURED and explicitly "not a red":
check-test-completenessneeds a saved
turbo run testlog it was not handed, andscripts/pm/check-half-states.mjsneeds a per-card comment fetch that neverhappened.
check:system-context-censuswas the one family this diff genuinely moved: the+50-line helper in
packages/runtime/src/domains/packages.tsshifted threeelevation-read anchors (
:95 -> :145,:128 -> :178,:246 -> :296). Repairedmechanically with the gate's own
--fix, which re-anchoredcontent/docs/permissions/system-context.mdx; the gate then reportsOK — 109 elevation read sites in 20 packages across 45 files, all anchored.That docs edit pulled 24 further doc families into the derivation, all of which
were run and are green.
Ablation (the pin fails without the fix).
manifest: toRecordManifest(manifest)was reverted to
manifest,ininstallPackage; the mutation was confirmed ondisk before running (anchor text
1 -> 0, injected text1, blob hash differingfrom
HEAD), the restore leg wasgit checkout HEAD -- <abs path>under anEXIT/INT/TERMtrap, and restoration was proved by an emptygit diff HEADplusgit hash-objectequality with theHEADblob. No rebuild was needed: the pinimports
./registryfrom source, not throughdist. Result:Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the commands were run directly, without the lock — a declared
narrowing, not a silent one. No serialization guarantee held for these runs, nor
for any sibling agent in this container while they ran.
Declared narrowing — repo-wide scans left to CI.
pnpm lintand the rest ofthe 62-family farm were not run locally; CI runs the farm exactly once on this
PR. What ran locally is the targeted set and the 60 gate families
named above.
Generated by Claude Code